var Switch = {
	start: function () {
		
	},
	
	init: function (nports, speed, name) {
		ARG_CHECK(arguments, 2, 3);
		
		this.speed = speed;
		this.name = name;
		
		this.ports = [];
		
		for (var i = 0; i < nports; i ++) {
			this.ports.push(new SwitchPort(this, i));
		}
		
		this.arpTable = {}; // mapping of ip -> port
		this.switches = {}; // mapping of portnumber -> switch
	},
	
	connect: function (host, port) {
		ARG_CHECK(arguments, 2, 2);
		
		this.ports[port].host = host;
		this.ports[port].isSwitch = false;
		this.arpTable[host.ip] = this.ports[port];

		for (var portnum in this.switches) {
			this.switches[portnum].addArp(host.ip, this);
		}
	},
	
	disconnect: function (ip) {
	},
	
	connectSwitch: function (lport, sw, rport) {
		ARG_CHECK(arguments, 3, 3);
		
		// local side
		this.ports[lport].host = sw;
		this.ports[lport].isSwitch = true;
		this.switches[lport] = sw;
		
		// remote side
		sw.ports[rport].host = this;
		sw.ports[rport].isSwitch = true;
		sw.switches[rport] = this;
		
		// get all the arp entries from the other switch
		for (var ip in sw.arpTable) {
			this.addArp(ip, sw);
		}
		
		for (var ip in this.arpTable) {
			sw.addArp(ip, this);
		}
	},
	
	addArp: function (ip, otherswitch) {
		ARG_CHECK(arguments, 2, 2);
		
		if (this.arpTable[ip]) {
			// If I already have a path to this ip:
			
			// if I am directly connected to this host, ignore the request
			if (this.arpTable[ip].host.ip == ip) {
				return;
			}
			// add an entry if the new path is shorter than current
			if (this.pathlength(ip) <= (otherswitch.pathlength(ip) + 1)) {
				return;
			}
			
			this.arpTable[ip] = null;
		}
		
		// add this entry
		for (var portnum in this.switches) {
			if (this.switches[portnum] == otherswitch) {
				this.arpTable[ip] = this.ports[portnum];
				break;
			}
		}
		
		// advertise to other switches
		for (var portnum in this.switches) {
			if (this.switches[portnum] == otherswitch) {
				continue;
			} else {
				this.switches[portnum].addArp(ip, this);
			}
		}
	},
	
	pathlength: function (ip) {
		var port = this.arpTable[ip];
		if (!port) return Math.Infinity;
		
		if (port.isSwitch) {
			return 1 + port.host.pathlength(ip);
		} else {
			return 1;
		}
	},
	
	resolve: function (ip) {
		var port = this.arpTable[ip];
		if (!port) return null;
		if (port.isSwitch) {
			return port.host.resolve(ip);
		} else {
			return port.host;
		}
	},

	
	transmit: function (senderIP, senderPort, destIP, destPort, size, data) {
		ARG_CHECK(arguments, 5, 8);
		
		var inPort = this.arpTable[senderIP];
		if (!inPort) {
			// ????
			return;
		}
		
		var outPort = this.arpTable[destIP];
		if (!outPort) {
			// ???
			return;
		}
		
		var packet = {src: senderIP, 
						sport: senderPort, 
						dst: destIP,
						dport: destPort,
						data: data,
						size: size,
						inPort: inPort,
						outPort: outPort};
		
		inPort.enqueue(packet);
	},
	
	failPort: function (port) {
		this.ports[port].alive = false;
	},
	
	recoverPort: function (port) {
		
	},
	
	fail: function () {
		
	},
	
	recover: function () {
		
	},
	
	freeport: function () {
		for (var i = 0; i < this.ports.length; i ++) {
			if (!this.ports[i].host) {
				return i;
			}
		}
		return -1;
	},
	
	printarp: function () {
		print("ARP table at " + this.id);
		for (var ip in this.arpTable) {
			var port = this.arpTable[ip];
			print(ip + " => Port " + port.id
					+ (port.isSwitch ? " (switch)" : ""));
		}
	},
	
	traceroute: function (src, dst) {
		ARG_CHECK(arguments, 2, 2);
		var path = [];
		var srcport = this.arpTable[src];
		if (!srcport) {return path;}
		path.push(this.id + ":" + srcport.id);
		
		var dstport = this.arpTable[dst];
		if (!dstport) { return path; }
		
		path.push(this.id + ":" + dstport.id);
		
		if (dstport.isSwitch) {
			path = path.concat(dstport.host.traceroute(src, dst));
		}
		return path;
	},
	
	report: function (writer) {
		for (var i = 0; i < this.ports.length; i++) {
			var port = this.ports[i];
			if (!port || !port.host) continue;
			writer("Switch port " + i 
					+ " connected to " + port.host.ip
					+ " enqueued = " + port.enqueued
					+ " dequeued = " + port.dequeued);
		}
	}
};

/*******************************************************************/
var TCP_MSS = 1024;

SwitchPort = function (sw, id) {
	this.sw = sw;
	this.id = id;
	this.host = null;
	this.isSwitch = false;
	this.inQueue = new Sim.Facility("InPort-" + this.id);
	this.outQueue = new Sim.Facility("OutPort-" + this.id);
	this.slotsize = TCP_MSS / this.sw.speed;
	this.enqueued = 0;
	this.dequeued = 0;
};

SwitchPort.prototype.enqueue = function (packet) {
	var nPackets = Math.ceil(packet.size / TCP_MSS);
	this.enqueued += nPackets;
	
	for (var i = nPackets; i > 0; i--) {
		this.transit(packet.outPort, packet, i == 1);
	}
};

SwitchPort.prototype.transit = function (outPort, packet, last) {
/*	if (packet && packet.data && packet.data.trace) {
		this.sw.log("Enqueue packet Port=" + this.id
				+ " last=" + last
				+ " src=" + packet.src);
	}
*/
	
	this.sw.useFacility(this.inQueue, this.slotsize) // spend time in inQueue
	.done(this.afterEnqueue, outPort, last)
	.setData(packet);
};

SwitchPort.prototype.afterEnqueue = function (last) {
		this.sw.useFacility(this.outQueue, this.slotsize) // spend time in outQueue
		.done(this.dequeue, this, last)
		.setData(this.callbackData);
}

SwitchPort.prototype.dequeue = function (last) {
	
	var packet = this.callbackData;
	if (!packet) return;
	if (!this.host) return;
	if (packet && packet.data && packet.data.trace) {
		this.sw.log("Deque packet Port=" + this.id
				+ " last=" + last
				+ " src=" + packet.src);
	}
	
	if (!this.isSwitch) {
		this.dequeued ++;
		// If this is a host
		if (!last) return;

		if (this.host.recv) {
			this.host.recv(packet.src,
					packet.sport,
					packet.dst,
					packet.dport,
					packet.data,
					packet.size);
		}
	
	//	delete packet;
	} else {
		var sw = this.host;
		var outPort = sw.arpTable[packet.dst];

//		sw.log("Enqueue switch packet Port=" + outPort.id
//				+ " last=" + last
//				+ " src=" + packet.src);
		
		// TODO outPort check
		this.sw.useFacility(outPort.outQueue, this.slotsize) // spend time in outQueue
		.done(this.dequeue, outPort, last)
		.setData(packet);
	}
};